跳到主要内容

Jenkins 微服务持续集成

本文中使用到的项目地址:https://gitee.com/alsritter/studyjenkins

启动时动态传入参数做配置

因为本地开发和上传到服务器的地址是不一样的(本地一般是 localhost,而远程环境是具体的服务 ip),所以可以通过 profile 写多套环境,不同的环境选择不同的 profile

1、maven 配置

<!--    maven多环境打包-->
<profiles>
<!--开发环境-->
<profile>
<id>dev</id>
<properties>
<profile.active>dev</profile.active>
</properties>
<!--没有mvn指令指定时,默认所属环境-->
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<!--生产环境-->
<profile>
<id>prod</id>
<properties>
<profile.active>prod</profile.active>
</properties>
</profile>
<!--测试环境-->
<profile>
<id>test</id>
<properties>
<profile.active>test</profile.active>
</properties>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
</profile>
</profiles>

2、application.yml 配置

spring:
application:
name: "testclient"
profiles:
active: @profile.active@

Jenkins 配置

其中 install -Pxxxxx,后面根据不同字段(prod,dev,test)来确定读取哪一个 application-xxxx 配置文件。

拉取项目源码

流水线脚本选择 SCM

然后添加一个 Jenkins 参数

然后在微服务项目的 根目录 加个 Jenkinsfile 使用脚本语法结构

创建 Jenkinsfile 文件

node {
def mvnHome
def git_auth = "6a169742-edf6-4a9f-b13d-91575692e9c9"
def git_url = "https://gitee.com/alsritter/studyjenkins.git"

stage('拉取代码') { // for display purposes
checkout([$class: 'GitSCM', branches: [[name: '*/${branch}']], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]]])
}
}

公共模块的处理

因为很多时候微服务,并不是只编译自身,还可能依赖了别的公共模块,因此需要连同这个公共模块一起编译

注意:spring-boot-maven-plugin 插件不能放在父工程,应该放在子工程,否则像公共模块这种没有 main 函数的模块会报错的

node {
def mvnHome
def git_auth = "6a169742-edf6-4a9f-b13d-91575692e9c9"
def git_url = "https://gitee.com/alsritter/studyjenkins.git"

stage('拉取代码') { // for display purposes
checkout([$class: 'GitSCM', branches: [[name: '*/${branch}']], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]]])
}

stage('编译安装公共子工程') {
sh "mvn -f common clean install"
}
}

补充知识:Maven 生命周期的 install 会将当前模块安装至本地仓库,以让其它项目依赖。该命令包含了 package 命令功能,不但会在项目路径下生成 class 文件和 jar 包,同时会在你的本地 Maven 仓库生成 jar 文件,供其他项目使用(如果没有设置过 Maven 本地仓库,一般在用户 /.m2 目录下。如果 a 项目依赖于 b 项目,那么 install b 项目时,会在本地仓库同时生成 pom 文件和 jar 文件,解决了上面打包 package 出错的问题)

选择微服务打包

因为一个微服务项目有很多子模块,所以这里创建一个选择参数,这些是项目的名称(注意!!子模块名称一定要全小写,也不能用驼峰,微服务对应的子模块也是,不能有大写,不然后面打包 docker 镜像会失败)

如下:

node {
def mvnHome
def git_auth = "6a169742-edf6-4a9f-b13d-91575692e9c9"
def git_url = "https://gitee.com/alsritter/studyjenkins.git"

stage('拉取代码') { // for display purposes
checkout([$class: 'GitSCM', branches: [[name: '*/${branch}']], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]]])
}

stage('编译安装公共子工程') {
sh "mvn -f common clean install"
}

stage('编译打包微服务工程') {
sh "mvn -f ${project_name} clean package"
}
}

使用 Dockerfile 编译、生成镜像

利用 dockerfile-maven-plugin 插件构建 Docker 镜像

1、在每个微服务项目的 pom.xml 加入 dockerfile-maven-plugin 插件

<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.10</version>
<configuration>
<repository>${project.artifactId}</repository>
<buildArgs>
<!-- 这里就是用来写构建 dockerfile 的参数的位置 -->
<!-- 例如这个 JAR_FILE 就是对应下面的 dockerfile 的 JAR_FILE -->
<JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
</configuration>
</plugin>

2、在每个微服务项目根目录下建立 Dockerfile 文件

#FROM java:8
FROM openjdk:8-jdk-alpine
# 设置编译镜像时加入的参数
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
# 别忘了配置端口
EXPOSE 8761
ENTRYPOINT ["java","-jar","/app.jar"]

3、修改下 Jenkinsfile,在上面这个打包的命令后面加个 dockerfile-maven-plugin 插件的触发命令

stage('编译打包微服务工程') {
sh "mvn -f ${project_name} clean package dockerfile:build"
}

如果报错,找不到 dockerfile 前缀的插件

找到 maven 的 conf/setting.xml,在 pluginGroups 标签内加入

<pluginGroups>
<pluginGroup>com.spotify</pluginGroup>
</pluginGroups>

如果报错 dockerfile-maven-plugin 没有权限

改变目录权限

chmod 777 /var/run/docker.sock

编译异常:导致这个错误的原因是 project.artifactId 可能包含了大写(注意!!不能有大写,像 testClient 这种驼峰也不行)

上传镜像

使用凭证管理 Harbor 私服账户和密码 先在凭证建立 Harbor 的凭证,在生成凭证代码

复制下这个凭证代码,粘贴到 Jenkinsfile 里面

然后通过脚本生成器生成这个

4、修改 Jenkinsfile 构建脚本

node {
def mvnHome
def git_auth = "6a169742-edf6-4a9f-b13d-91575692e9c9"
def git_url = "https://gitee.com/alsritter/studyjenkins.git"

//Harbor私服地址
def harbor_url = "192.168.211.130:85"
//Harbor的项目名称
def harbor_project_name = "studyjenkins"
//构建版本的名称
def tag = "latest"
//Harbor的凭证
def harbor_auth = "c9f28cdc-2f79-456c-b233-4e60356941a0"

stage('拉取代码') { // for display purposes
checkout([$class: 'GitSCM', branches: [[name: '*/${branch}']], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]]])
}

stage('编译安装公共子工程') {
sh "mvn -f common clean install"
}

stage('编译打包微服务工程') {
//定义镜像名称
def imageName = "${project_name}:${tag}" as String
sh "mvn -f ${project_name} clean package dockerfile:build"
//给镜像打标签
sh "docker tag ${imageName} ${harbor_url}/${harbor_project_name}/${imageName}"

withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
//登录
sh "docker login -u ${username} -p ${password} ${harbor_url}"
//上传镜像
sh "docker push ${harbor_url}/${harbor_project_name}/${imageName}"

sh "echo 镜像上传成功"
}

//删除本地镜像
sh "docker rmi -f ${imageName}"
sh "docker rmi -f ${harbor_url}/${harbor_project_name}/${imageName}"
}
}

拉取镜像和发布应用

1、安装 Publish Over SSH 插件,使之可以实现远程发送 Shell 命令(记得重启) 2、配置远程部署服务器

拷贝公钥到远程服务器(在 Jenkins 的公钥拷贝到生产服务器上去)

# 如果之前沒有建立過 RSA 的 key,就可以先建一把:
ssh-keygen -t rsa -b 4096

# 用于生产的服务器
ssh-copy-id 192.168.211.130

3、系统配置 => 添加远程服务器

下面填上远程服务器的地址,然后点测试连接

在期间遇到的坑: 如果显示:

jenkins.plugins.publish_over.BapPublisherException: Failed to add SSH key. Message [invalid privatekey: [B@60373f7]

这是由于生成密钥的 openssh 的版本过高

需要使用以下命令来生成密钥:

ssh-keygen -m PEM -t rsa -b 4096

-m 参数指定密钥的格式, PEM 是 rsa 之前使用的旧格式,4096 为长度。

再次点击就能生成成功了~

添加一个 port 参数

编写 deploy.sh 部署脚本

#! /bin/sh
# 要空一格

#接收外部参数
harbor_url=$1
harbor_project_name=$2
project_name=$3
tag=$4
port=$5

imageName=$harbor_url/$harbor_project_name/$project_name:$tag

echo "$imageName"

#查询容器是否存在,存在则删除
# grew 命令是文本搜索工具
# awk 命令是一种处理文本文件的语言
containerId=`docker ps -a | grep -w ${project_name}:${tag} | awk '{print $1}'`
if [ "$containerId" != "" ] ; then
#停掉容器
docker stop $containerId

#删除容器
docker rm $containerId

echo "成功删除容器"
fi

#查询镜像是否存在,存在则删除
imageId=`docker images | grep -w $project_name | awk '{print $3}'`

if [ "$imageId" != "" ] ; then
#删除镜像
docker rmi -f $imageId
echo "成功删除镜像"
fi

# 登录Harbor私服
# 这里账号密码
docker login -u alsritter -p Abc123456 $harbor_url

# 下载镜像
docker pull $imageName

# 启动容器
docker run -di -p $port:$port $imageName

echo "容器启动成功"

上传 deploy.sh 文件到部署目标的 /home/alsritter/workProject/jenkins_shell/ 目录下,且文件至少有执行权限!

# 添加执行权限
chmod +x deploy.sh

补充:jenkins 部署报错 Exec exit status not zero. Status [-1]

这是因为这个脚本没有 docker 权限(管理员权限才能执行)

# 添加 docker group:
sudo groupadd docker
# 将当前用户添加到docker组:语法:gpasswd -a user_name group_name
sudo gpasswd -a ${USER} docker # (这个 ${USER} 是默认用户)
# 重启docker服务:
sudo service docker restart

# 因为要刷新,所以这里应该切换或者退出当前账户再从新登入
su root #切换到root用户
su ${USER} #再切换到原来的应用用户以上配置才生效(这个 ${USER} 是默认用户)

修改 Jenkinsfile 构建脚本,生成远程调用模板代码(不用管报红的警告,直接点生成)

stage('部署') {
// 这里调用远程服务并执行一条命令(除了 execCommand 其它都是默认配置)
sshPublisher(publishers: [sshPublisherDesc(configName: 'master_server',
transfers: [sshTransfer(cleanRemote: false, excludes: '',

// 这里调用 deploy.sh 脚本,并依次传入参数给这个脚本
execCommand: "/home/alsritter/workProject/jenkins_shell/deploy.sh $harbor_url $harbor_project_name $project_name $tag $port",

execTimeout: 120000, flatten: false,
makeEmptyDirs: false, noDefaultExcludes: false,
patternSeparator: '[, ]+', remoteDirectory: '',
remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')],
usePromotionTimestamp: false,
useWorkspaceInPromotion: false,
verbose: false)])
}

手动测试脚本

./deploy.sh 192.168.211.130:85 studyjenkins eurekaserver latest 8761

如果出现以下报错,要在出错的主机上配置 非安全访问方式 insecure-registries

sudo vi /etc/docker/daemon.json

然后重启 Docker

# 注意:暂停 docker 前需要到 harbor 目录里面手动暂停这个服务,否则会重启失败
cd /opt/harbor/
# 停止
sudo docker-compose stop


sudo systemctl restart docker

# 再启动
sudo docker-compose up -d
# 检测下当前状态
sudo docker-compose ps

再次运行,打包成功!

Jenkins 部署测试

访问这个服务测试

http://192.168.211.130:8761/

访问成功

完整的 Jenkinsfile 脚本

node {
def mvnHome
def git_auth = "6a169742-edf6-4a9f-b13d-91575692e9c9"
def git_url = "https://gitee.com/alsritter/studyjenkins.git"

//Harbor私服地址
def harbor_url = "192.168.211.130:85"
//Harbor的项目名称
def harbor_project_name = "studyjenkins"
//构建版本的名称
def tag = "latest"
//Harbor的凭证
def harbor_auth = "c9f28cdc-2f79-456c-b233-4e60356941a0"

stage('拉取代码') { // for display purposes
checkout([$class: 'GitSCM', branches: [[name: '*/${branch}']], extensions: [], userRemoteConfigs: [[credentialsId: "${git_auth}", url: "${git_url}"]]])
}

stage('编译安装公共子工程') {
sh "mvn -f common clean install"
}

stage('编译打包微服务工程') {
//定义镜像名称
def imageName = "${project_name}:${tag}" as String
sh "mvn -f ${project_name} clean package dockerfile:build"
//给镜像打标签
sh "docker tag ${imageName} ${harbor_url}/${harbor_project_name}/${imageName}"

withCredentials([usernamePassword(credentialsId: "${harbor_auth}", passwordVariable: 'password', usernameVariable: 'username')]) {
//登录
sh "docker login -u ${username} -p ${password} ${harbor_url}"
//上传镜像
sh "docker push ${harbor_url}/${harbor_project_name}/${imageName}"

sh "echo 镜像上传成功"
}

//删除本地镜像
sh "docker rmi -f ${imageName}"
sh "docker rmi -f ${harbor_url}/${harbor_project_name}/${imageName}"
}

stage('部署') {
sh "echo 传入的参数为: $harbor_url $harbor_project_name $project_name $tag $port"
// 这里调用远程服务并执行一条命令(除了 execCommand 其它都是默认配置)
sshPublisher(publishers: [sshPublisherDesc(configName: 'master_server',
transfers: [sshTransfer(cleanRemote: false, excludes: '',

// 这里调用 deploy.sh 脚本,并依次传入参数给这个脚本
execCommand: "/home/alsritter/workProject/jenkins_shell/deploy.sh $harbor_url $harbor_project_name $project_name $tag $port",

execTimeout: 120000, flatten: false,
makeEmptyDirs: false, noDefaultExcludes: false,
patternSeparator: '[, ]+', remoteDirectory: '',
remoteDirectorySDF: false, removePrefix: '', sourceFiles: '')],
usePromotionTimestamp: false,
useWorkspaceInPromotion: false,
verbose: false)])

sh "echo 脚本执行"
}
}

Reference

Jenkins免密登录的坑